package de.justphil.tcg.tcgserver.game.mechanics.event;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

import net.jcip.annotations.Immutable;
import de.justphil.tcg.tcgserver.game.mechanics.game.Attack;
import de.justphil.tcg.tcgserver.game.mechanics.game.BoardPosition;
import de.justphil.tcg.tcgserver.game.mechanics.game.Game;
import de.justphil.tcg.tcgserver.game.mechanics.game.GameDataSerializable;
import de.justphil.tcg.tcgserver.game.mechanics.monster.Monster;
import de.justphil.tcg.tcgserver.game.mechanics.property.Property;
import de.justphil.tcg.tcgserver.game.mechanics.util.tools.datatypes.SerializableInteger;

@Immutable
public class Event implements GameDataSerializable {

	/**
	 * A Non-Event which will never occur.
	 */
	public static final int EVENT_NOTHING = 0;

	/**
	 * This Event type carries the newly killed Monster as data object.
	 */
	public static final int EVENT_MONSTER_KILLED = 1;

	/**
	 *
	 */
	public static final int EVENT_DECK_EXHAUSTED = 2;

	/**
	 * This Event type carries the newly placed Monster as data object.
	 */
	public static final int EVENT_MONSTER_PLACED = 3;

	/**
	 * This Event type carries the newly removed Monster as data object.
	 */
	public static final int EVENT_MONSTER_REMOVED = 4;

	/**
	 * This Event type carries an array as data which contains the attacker and
	 * the target.
	 */
	public static final int EVENT_MONSTER_HIT = 5;

	/**
	 * This Event type carries the newly scheduled Event as data object.
	 */
	public static final int EVENT_EVENT_SCHEDULED = 6;

	/**
	 * This Event type carries the Monster which came to turn as data object.
	 */
	public static final int EVENT_MONSTERS_TURN = 7;

	/**
	 * This Event type carries an Attack.
	 */
	public static final int EVENT_ATTACK_INVOKED = 8;

	/**
	 * This Event type carries an Attack.
	 */
	public static final int EVENT_ATTACK_FINISHED = 9;

	/**
	 * This Event type carries the Monster which got a new property.
	 */
	public static final int EVENT_PROPERTY_ACTIVATED = 10;

	/**
	 * This Event type carries another Event which has finished as data.
	 */
	public static final int EVENT_EVENT_FINISHED = 11;

	/**
	 * This Event type carries an Attack.
	 */
	public static final int EVENT_ATTACK = 12;

	/**
	 * This Event type carries an array as data which contains the attacker and
	 * the target.
	 */
	public static final int EVENT_MISS = 13;

	/**
	 * This Event type carries the monster which died.
	 */
	public static final int EVENT_MONSTER_DIED = 14;

	/**
	 * This Event does not carry data (data == null).
	 */
	public static final int EVENT_START = 15;

	/**
	 * This Event does not carry data (data == null).
	 */
	public static final int EVENT_END = 16;

	/**
	 * This Event type carries the Monster which lost a property.
	 */
	public static final int EVENT_PROPERTY_DEACTIVATED = 17;

	/**
	 * This Event type carries the Monster which lost a property.
	 */
	public static final int EVENT_REMOVE_IF_DEAD = 18;

	/**
	 * This Event type carries the Monster which will be healed.
	 */
	public static final int EVENT_HEALING = 19;

	/**
	 * This Event type carries an int array {player, slot}.
	 */
	public static final int EVENT_SLOT_SELECTED = 20;

	/**
	 * This Event type carries a BoardPosition.
	 */
	public static final int EVENT_DRAW_NEW_MONSTER = 21;

	/**
	 * This Event type carries a BoardPosition.
	 */
	public static final int EVENT_WAITING_ON_SLOT_SELECTION = 22;

	/**
	 * This Event type carries an the player as int.
	 */
	public static final int EVENT_WAITING_ON_CONFIRM = 23;

	// /**
	// * Gets the String name to any Event type.
	// *
	// * @param type
	// * the Event type
	// * @return the name of the Event type as String
	// */
	// public static String getTypeAsString(int type) {
	//
	// }

	private int type;

	private Object data;
	private String typeString;
	private static Object TYPE_CHECK = new Object();

	public static String getTypeAsString(int type) {

		return new Event(type, TYPE_CHECK).getTypeAsString();
	}

	public Event() {

	}

	public Event(int type, Object data) {

		typeString = analyseEvent(type, data);

		this.type = type;
		this.data = data;
	}

	/**
	 * Validating the Event data.
	 * 
	 * @param type
	 *            the type
	 * @param data
	 *            the data Object
	 * @return the Event type as String
	 */
	private String analyseEvent(int type, Object data) {

		if (data != null && data != TYPE_CHECK) {

			assert GameDataSerializable.class.isInstance(data);
		}

		switch (type) {

		case EVENT_MONSTER_KILLED:
			assert Monster.class.isInstance(data) || data == TYPE_CHECK;
			return "MONSTER_KILLED";
		case EVENT_DECK_EXHAUSTED:
			assert data == null || data == TYPE_CHECK;
			return "DECK_EXHAUSTED";
		case EVENT_MONSTER_PLACED:
			assert Monster.class.isInstance(data) || data == TYPE_CHECK;
			return "MONSTER_PLACED";
		case EVENT_MONSTER_REMOVED:
			assert Monster.class.isInstance(data) || data == TYPE_CHECK;
			return "MONSTER_REMOVED";
		case EVENT_MONSTER_HIT:
			assert Attack.class.isInstance(data) || data == TYPE_CHECK;
			return "MONSTER_HIT";
		case EVENT_EVENT_SCHEDULED:
			assert Event.class.isInstance(data) || data == TYPE_CHECK;
			return "EVENT_SCHEDULED";
		case EVENT_MONSTERS_TURN:
			assert Monster.class.isInstance(data) || data == TYPE_CHECK;
			return "MONSTERS_TURN";
		case EVENT_ATTACK_INVOKED:
			assert Attack.class.isInstance(data) || data == TYPE_CHECK;
			return "ATTACK_INVOKED";
		case EVENT_ATTACK_FINISHED:
			assert Attack.class.isInstance(data) || data == TYPE_CHECK;
			return "ATTACK_FINISHED";
		case EVENT_PROPERTY_ACTIVATED:
			assert Monster.class.isInstance(data) || data == TYPE_CHECK;
			return "PROPERTY_ACTIVATED";
		case EVENT_ATTACK:
			assert Attack.class.isInstance(data) || data == TYPE_CHECK;
			return "ATTACK";
		case Event.EVENT_MISS:
			assert Attack.class.isInstance(data) || data == TYPE_CHECK;
			return "MISS";
		case EVENT_MONSTER_DIED:
			assert Monster.class.isInstance(data) || data == TYPE_CHECK;
			return "MONSTER_DIED";
		case EVENT_START:
			assert data == null || data == TYPE_CHECK;
			return "START";
		case EVENT_END:
			assert data == null || data == TYPE_CHECK;
			return "END";
		case EVENT_PROPERTY_DEACTIVATED:
			assert Monster.class.isInstance(data) || data == TYPE_CHECK;
			return "PROPERTY_DEACTIVATED";
		case EVENT_REMOVE_IF_DEAD:
			assert Monster.class.isInstance(data) || data == TYPE_CHECK;
			return "REMOVE_IF_DEAD";
		case EVENT_HEALING:
			assert Monster.class.isInstance(data) || data == TYPE_CHECK;
			return "HEALING";
		case EVENT_SLOT_SELECTED:
			assert BoardPosition.class.isInstance(data) || data == TYPE_CHECK;
			return "SLOT_SELECTED";
		case EVENT_DRAW_NEW_MONSTER:
			assert BoardPosition.class.isInstance(data) || data == TYPE_CHECK;
			return "DRAW_NEW_MONSTER";
		case EVENT_WAITING_ON_SLOT_SELECTION:
			assert SerializableInteger.class.isInstance(data)
					|| data == TYPE_CHECK;
			return "WAITING_ON_SLOT_SELECTION";
		case EVENT_WAITING_ON_CONFIRM:
			assert SerializableInteger.class.isInstance(data)
					|| data == TYPE_CHECK;
			return "WAITING_ON_CONFIRM";
		default:
			assert false : "Event type " + type + " is unknown!";
			return "NONE";
		}
	}

	/**
	 * Gets the type of this Event
	 * 
	 * @return the type of this Event
	 */
	public int getType() {

		return this.type;
	}

	/**
	 * 
	 * Gets the data Object of this Event.
	 * 
	 * @return the data Object of this Event. Dependent on the Event type this
	 *         Object can be cast to the specific Class.
	 */
	public Object getData() {

		return this.data;
	}

	/**
	 * Gets the String name of this Event's type.
	 * 
	 * @return the String name of this Event's type.
	 */
	public String getTypeAsString() {

		return this.typeString;
	}

	@Override
	public void writeData(Game game, DataOutput out) throws IOException {
	
		out.writeInt(type);
	
		int dataType = EventDataFactory.getDataType(data);
		out.writeInt(dataType);
		
		if (dataType == EventDataFactory.MONSTER) {
			Monster m = (Monster) data;
			game.getDataSerializableManager().writeMonster(m, game, out);
		}
		else {
			assert !(data instanceof Monster); 
			assert !(data instanceof Property); 
			
			if (dataType != EventDataFactory.NULL) {
				((GameDataSerializable) data).writeData(game, out);
			}
		}
		
	}

	@Override
	public void readData(Game game, DataInput in) throws IOException {
	
		type = in.readInt();
		int dataType = in.readInt();
		
		if (dataType == EventDataFactory.MONSTER) {
			data = game.getDataSerializableManager().readMonster(game, in);
		}
		else {
			if (dataType != EventDataFactory.NULL) {
				assert !(data instanceof Monster); 
				assert !(data instanceof Property); 

				data = EventDataFactory.getData(dataType);
				((GameDataSerializable) data).readData(game, in);
			} else {
				data = null;
			}
		}
		typeString = analyseEvent(type, data);
	}

	/**
	 * Two Events equal each other when their types and objects are equal.
	 * 
	 */
	@Override
	public boolean equals(Object obj) {

		if (!Event.class.isInstance(obj)) {

			return false;
		}
		Event event = (Event) obj;

		return event.type == this.type
				&& ((event.data != null && event.data.equals(this.data)) || (event.data == null && this.data == null));
	}

	/**
	 * int result = 17; result = 31 * result + type; result = 31 * result +
	 * data.hashCode(); return result
	 */
	@Override
	public int hashCode() {

		int result = 17;
		result = 31 * result + type;
		result = 31 * result + data.hashCode();
		return result;
	}

	@Override
	public String toString() {

		return "T:|" + getTypeAsString() + "|D:(" + getData() + ")";
	}
}
